Skip to content

fix: compute file_size_bytes from the live stride, not PAGE_SIZE (#94) - #129

Merged
Xof merged 1 commit into
mainfrom
fix/94-file-size-stride
Jul 31, 2026
Merged

fix: compute file_size_bytes from the live stride, not PAGE_SIZE (#94)#129
Xof merged 1 commit into
mainfrom
fix/94-file-size-stride

Conversation

@Xof

@Xof Xof commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Closes #94 (CRYPTO-5, BUG/correctness).

The defect

Chisel::file_size_bytes and Stats.file_size_bytes both did
page_count.saturating_mul(PAGE_SIZE as u64). But PageIo::page_count
returns stride-units, and an encrypted database is re-strided to
ENC_PAGE_SIZE (8232) at open — so both public accessors under-reported
every encrypted database's size by 0.49%, unbounded in absolute terms
(40 MB of error at one million pages).

open_existing already computes this correctly for its FileSizeMismatch
arithmetic and its comment names the exact hazard:

multiply by the CURRENT stride, not the hardcoded PAGE_SIZE … reporting
them as PAGE_SIZE bytes would understate an encrypted file's true size

The two public accessors just never got the same treatment.

The fix

Adds TransactionManager::file_stride() — an infallible in-memory read of
PageIo::stride(), mirroring how open_existing reaches it — and
multiplies by that at both call sites. Docs on Chisel::stats,
Chisel::file_size_bytes and Stats::file_size_bytes updated to say
page_count × stride and name both strides.

No behaviour change for plaintext databases: stride is PAGE_SIZE there.

Test

file_size_bytes_matches_stat_for_both_strides asserts both accessors
equal fs::metadata(path).len(), for a plaintext and an encrypted database,
after growing each past the superblock region.

Asserting against stat(2) rather than a computed constant is what makes
the test load-bearing: PAGE_SIZE arithmetic can only agree with stat in
the plaintext case. Counterfactual — reverting just the
file_size_bytes line to PAGE_SIZE:

assertion `left == right` failed: enc.db: file_size_bytes() disagrees with stat(2)
  left: 1081344
 right: 1086624

(132 pages × 8192 vs × 8232.) The plaintext arm guards the other direction.

Verification

cargo test 682 passing, cargo clippy --all-targets -- -D warnings clean,
cargo fmt --check clean.


First of a stacked series addressing #94#103.

`Chisel::file_size_bytes` and `Stats.file_size_bytes` both multiplied
`PageIo::page_count` by the hardcoded `PAGE_SIZE`. That count is in
stride-units, and an encrypted database runs at `ENC_PAGE_SIZE` (8232)
from birth, so every encrypted database under-reported its own on-disk
size by 0.49% — 40 MB at one million pages. `open_existing` already got
this right in its `FileSizeMismatch` arithmetic and even names the
hazard in a comment; the two public accessors did not.

Adds `TransactionManager::file_stride()` and multiplies by it at both
sites, so the value now matches `stat(2)` for plaintext and encrypted
databases alike — which is what `Stats.file_size_bytes` ("Raw size of
the database file on disk") and README ("Physical size of the database
file in bytes") have always promised.

The new test asserts against `fs::metadata(path).len()` rather than a
computed constant: PAGE_SIZE arithmetic can only agree with stat in the
plaintext case, so the encrypted arm fails on the old code and the
plaintext arm catches an over-correction in the other direction.

Closes #94.
@github-actions

Copy link
Copy Markdown

🚦 Bench results: PR vs main

⚠️ 17 regression(s) detected across 6 scenario/mode pair(s)

Scenario Mode Δ throughput Worst Δ
ycsb-a redb-strict -71.7% p99 +2624.5% ⚠️
document-store chisel-strict -55.1% p99 +733.4% ⚠️
document-store redb-strict -52.8% p99 +180.3% ⚠️
ycsb-a chisel-strict -13.4% p99 +47.4% ⚠️
ycsb-b chisel-mem -9.4% p99 +11.5% ⚠️
ycsb-b chisel-strict -6.0% throughput -6.0% ⚠️
document-store chisel-mem -1.9%
document-store sqlite-strict +117.7%
mutation-log chisel-mem -0.8%
mutation-log chisel-strict +35.2%
mutation-log redb-strict +76.6%
mutation-log sqlite-strict +51.0%
ycsb-a chisel-mem +2.8%
ycsb-a sqlite-strict +7.7%
ycsb-b redb-strict +162.1%
ycsb-b sqlite-strict -2.0%
Per-scenario detail (4 metrics × cells)

document-store

Mode Throughput p50 p95 p99
chisel-mem 25879 ops/s → 25387 ops/s (-1.9%) 5.7 µs → 5.9 µs (+2.7%) 75.0 µs → 76.0 µs (+1.3%) 392.3 µs → 408.0 µs (+4.0%)
chisel-strict 1314 ops/s → 590 ops/s (-55.1%) ⚠️ 10.4 µs → 12.0 µs (+15.8%) ⚠️ 1.31 ms → 1.76 ms (+33.9%) ⚠️ 4.14 ms → 34.51 ms (+733.4%) ⚠️
redb-strict 2509 ops/s → 1185 ops/s (-52.8%) ⚠️ 10.4 µs → 11.7 µs (+11.9%) ⚠️ 714.3 µs → 818.2 µs (+14.5%) ⚠️ 3.70 ms → 10.39 ms (+180.3%) ⚠️
sqlite-strict 623 ops/s → 1357 ops/s (+117.7%) 12.5 µs → 12.0 µs (-4.6%) 1.17 ms → 641.6 µs (-45.1%) 40.05 ms → 10.05 ms (-74.9%)

mutation-log

Mode Throughput p50 p95 p99
chisel-mem 73389 ops/s → 72815 ops/s (-0.8%) 16.9 µs → 17.0 µs (+0.4%) 19.2 µs → 19.3 µs (+0.5%) 28.9 µs → 28.9 µs (+0.1%)
chisel-strict 279 ops/s → 378 ops/s (+35.2%) 798.1 µs → 774.5 µs (-3.0%) 4.60 ms → 2.67 ms (-41.9%) 95.15 ms → 62.52 ms (-34.3%)
redb-strict 878 ops/s → 1550 ops/s (+76.6%) 212.2 µs → 204.1 µs (-3.8%) 736.8 µs → 571.8 µs (-22.4%) 20.15 ms → 3.34 ms (-83.4%)
sqlite-strict 909 ops/s → 1372 ops/s (+51.0%) 194.7 µs → 187.5 µs (-3.7%) 924.5 µs → 546.0 µs (-40.9%) 18.75 ms → 3.81 ms (-79.7%)

ycsb-a

Mode Throughput p50 p95 p99
chisel-mem 39414 ops/s → 40531 ops/s (+2.8%) 32.2 µs → 29.3 µs (-9.0%) 55.0 µs → 52.5 µs (-4.5%) 146.8 µs → 150.5 µs (+2.5%)
chisel-strict 571 ops/s → 494 ops/s (-13.4%) ⚠️ 539.0 µs → 521.7 µs (-3.2%) 1.87 ms → 1.82 ms (-2.6%) 33.45 ms → 49.29 ms (+47.4%) ⚠️
redb-strict 2678 ops/s → 759 ops/s (-71.7%) ⚠️ 172.9 µs → 175.6 µs (+1.6%) 330.9 µs → 701.8 µs (+112.1%) ⚠️ 1.57 ms → 42.66 ms (+2624.5%) ⚠️
sqlite-strict 295853 ops/s → 318617 ops/s (+7.7%) 3.3 µs → 3.0 µs (-9.3%) 4.6 µs → 4.4 µs (-4.9%) 5.4 µs → 5.3 µs (-0.8%)

ycsb-b

Mode Throughput p50 p95 p99
chisel-mem 320104 ops/s → 289857 ops/s (-9.4%) ⚠️ 1.2 µs → 1.3 µs (+7.4%) ⚠️ 31.0 µs → 34.1 µs (+9.8%) 42.7 µs → 47.6 µs (+11.5%) ⚠️
chisel-strict 3195 ops/s → 3004 ops/s (-6.0%) ⚠️ 2.8 µs → 2.8 µs (+2.1%) 605.3 µs → 559.9 µs (-7.5%) 1.64 ms → 1.58 ms (-4.0%)
redb-strict 11280 ops/s → 29568 ops/s (+162.1%) 1.9 µs → 1.9 µs (+1.1%) 175.9 µs → 178.8 µs (+1.7%) 262.7 µs → 255.4 µs (-2.8%)
sqlite-strict 390144 ops/s → 382363 ops/s (-2.0%) 2.5 µs → 2.6 µs (+1.9%) 3.7 µs → 3.8 µs (+2.2%) 4.3 µs → 4.4 µs (+1.7%)
Generated by chisel-bench-diff at 2026-07-29T20:04:54Z. Compares PR HEAD against main. Never blocks merge — signal, not gate. Thresholds: throughput 5%, p50 5%, p95 10%, p99 10%.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] file_size_bytes and Stats.file_size_bytes multiply stride-unit page counts by PAGE_SIZE, under-reporting every encrypted database's file size

1 participant